package devcken.net.socket;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteOrder;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

import devcken.net.NetworkOrder;
import devcken.text.Encoding;
import devcken.utility.BitConverter;

import org.apache.derby.iapi.util.ByteArray;

public class Header
{
	/**
	 * 패킷을 구분하는 구분자 상수를 나타낸다.
	 */
	public static final byte[] _PACKET_SEPERATOR = new byte[] { (byte)255, 0, (byte)255, 0, (byte)255 };
	
	/**
	 * 패킷 아이디의 char 길이 상수를 나타낸다.
	 */
	public final static int _ID_CHAR_LENGTH = 10;
	
	/**
	 * 일반 헤더 클래스의 기본 크기 상수를 나타낸다.
	 */
	public final static int _DEFAULT_HEADER_CLASS_SIZE = _ID_CHAR_LENGTH + PacketType.Size + EncryptionOptions.Size + Integer.SIZE + Integer.SIZE + Integer.SIZE;
	
	/**
	 * 파일 헤더 클래스의 기본 크기 상수를 나타낸다.
	 */
	public final static int _FILE_HEADER_CLASS_SIZE = _ID_CHAR_LENGTH + PacketType.Size + EncryptionOptions.Size + Integer.SIZE + Long.SIZE + Integer.SIZE;

	/**
	 * 파일 헤더 클래스의 기본 크기 상수를 나타낸다.
	 */
	public final static int _CHUNKED_HEADER_CLASS_SIZE = _ID_CHAR_LENGTH + PacketType.Size + EncryptionOptions.Size + Integer.SIZE + Long.SIZE + Integer.SIZE + Integer.SIZE + Integer.SIZE;
	
	/**
	 * 패킷의 아이디를 나타낸다. 아이디는 각 partial 패킷마다 동일하다.
	 */
	private char[] _id;
	
	/**
	 * 패킷 유형을 나타낸다.
	 */
	private PacketType _packetType;
	
	/**
	 * 암호화 옵션을 나타낸다.
	 */
	private EncryptionOptions _encryptionOption;
	
	/**
	 * Header의 크기를 나타낸다.
	 */
	private int _headerSize;
	
	/**
	 * Body의 크기를 나타낸다.
	 */
	private int _bodySize;
	
	/**
	 * 송신하려는 데이터 객체 타입의 크기를 나타낸다.
	 */
	private int _dataTypesSize;
	
	/**
	 * 송신하려는 데이터의 객체 타입을 나타낸다.
	 */
	private String _dataType;
	
	/**
	 * 파일의 크기를 나타낸다.
	 */
	private long _fileSize;
	
	/**
	 * 파일 이름의 크기를 나타낸다.
	 */
	private int _fileNameSize;
	
	/**
	 * 확장자를 포함하는 파일 이름을 나타낸다.
	 */
	private String _fileName;
	
	/**
	 * 파일을 부분적으로 나눌 때의 인덱스를 나타낸다.
	 */
	private int _chunkedIndex;
	
	/**
	 * 파일을 부분적으로 나눌 때의 크기를 나타낸다.
	 */
	private int _chunkedSize;
	
	private Header(char[] id, PacketType packetType, EncryptionOptions encryptionOption)
	{
		this._id = id;
		
		this._packetType = packetType;
		this._encryptionOption = encryptionOption;
	}
	
	private Header(char[] id, int bodySize, String dataType, PacketType packetType, EncryptionOptions encryptionOption)
	{
		this(id, packetType, encryptionOption);
		
		this._bodySize = bodySize;
		
		try
		{
			this._dataTypesSize = dataType.getBytes("utf-8").length;
		}
		catch (UnsupportedEncodingException error)
		{
			
		}
		
		this._dataType = dataType;
	}
	
	private Header(char[] id, long fileSize, String fileName)
	{
		this(id, PacketType.File, EncryptionOptions.None);
		
		this._fileSize = fileSize;
		
		try
		{
			this._fileNameSize = fileName.getBytes("utf-8").length;
		}
		catch (UnsupportedEncodingException error)
		{
			
		}
		this._fileName = fileName;
	
		this._headerSize = _PACKET_SEPERATOR.length + _FILE_HEADER_CLASS_SIZE + this._fileNameSize;
	}
	
	/**
	 * 패킷 아이디를 생성한다.
	 * @see 패킷 아이디는 HHmmssfff 포맷의 생성 시간과 한 자리 숫자의 mark로 구성된다.
	 * @return 새로 생성된 패킷 아이디를 반환한다.
	 */
	private static char[] createPacketId()
	{
		char[] id = new char[_ID_CHAR_LENGTH];
		
		Date now = new Date();
		
		Random random = new Random((int)now.getTime());
		
		int randomMark = random.nextInt(9);
		
		SimpleDateFormat dateFormat = new SimpleDateFormat("HHmmssfff");
		
		char[] nowCharArray = dateFormat.format(now).toCharArray();
		
		System.arraycopy(nowCharArray, 0, id, 0, nowCharArray.length);
		
		id[9] = (char)randomMark;
		
		return id;
	}
	
	/**
	 * 내부 헤더를 사용하지 않는 경우에 사용되는 헤더를 생성한다.
	 * @return
	 */
	static Header generateHeader()
	{
		return new Header(null, PacketType.Raw, EncryptionOptions.None);
	}
	
	/**
	 * 헤더를 생성한다.
	 * @param bodySize 원문의 크기를 설정한다.
	 * @param dataType 전송하려는 데이터(원문)의 타입을 설정한다.
	 * @param packetType 패킷 유형을 설정한다.
	 * @param encryptionOption 암호화 옵션을 설정한다.
	 * @param headerBinary 헤더의 바이너리를 참조한다.
	 * @param isLittleEndian Little Endian을 사용하는지 여부를 설정한다.
	 * @return 생성된 헤더가 반환된다.
	 */
	static Header generateHeader(int bodySize, String dataType, PacketType packetType, EncryptionOptions encryptionOption, ByteArray headerBinary, boolean isLittleEndian)
	{
		Header header = new Header(createPacketId(), bodySize, dataType, packetType, encryptionOption);
		
		try
		{
			ByteArrayOutputStream outStream = new ByteArrayOutputStream(header.getHeaderSize());
			
			outStream.write(_PACKET_SEPERATOR, 0, _PACKET_SEPERATOR.length);
			
			byte[] idArray = new String(header.getId()).getBytes("utf-8");
			
			outStream.write(idArray, 0, idArray.length);
			
			outStream.write(header.getPacketType().value);
			
			outStream.write(header.getEncryptionOption().value);
			
			ByteBuffer headerSizeByteBuffer = ByteBuffer.allocate(1024);
			
			headerSizeByteBuffer.put(BitConverter.getBytes(header.getHeaderSize()));
			
			byte[] headerSizeBinary = isLittleEndian ? headerSizeByteBuffer.order(ByteOrder.LITTLE_ENDIAN).array() : headerSizeByteBuffer.array();
			
			outStream.write(headerSizeBinary, 0, headerSizeBinary.length);
			
			ByteBuffer bodySizeByteBuffer = ByteBuffer.allocate(1024);
			
			bodySizeByteBuffer.put(BitConverter.getBytes(header.getBodySize()));
			
			byte[] bodySizeBinary = isLittleEndian ? bodySizeByteBuffer.order(ByteOrder.LITTLE_ENDIAN).array() : bodySizeByteBuffer.array();
			
			outStream.write(bodySizeBinary, 0, bodySizeBinary.length);
			
			ByteBuffer dataTypeSizeByteBuffer = ByteBuffer.allocate(1024);
			
			dataTypeSizeByteBuffer.put(BitConverter.getBytes(header.getDataTypesSize()));
			
			byte[] dataTypeSizeBinary = isLittleEndian ? dataTypeSizeByteBuffer.order(ByteOrder.LITTLE_ENDIAN).array() : dataTypeSizeByteBuffer.array();
			
			outStream.write(dataTypeSizeBinary, 0, dataTypeSizeBinary.length);
			
			byte[] dataTypeBinary = header.getDataType().getBytes("utf-8");
			
			outStream.write(dataTypeBinary, 0, dataTypeBinary.length);
			
			if (headerBinary == null)
			{
				headerBinary = new ByteArray();
			}
			
			headerBinary.setLength(header.getHeaderSize());
			
			ByteArrayInputStream inputStream = new ByteArrayInputStream(outStream.toByteArray());
			
			byte[] headerBinaryTemp = new byte[header.getHeaderSize()];
			
			inputStream.read(headerBinaryTemp, 0, headerBinaryTemp.length);
			
			headerBinary.setBytes(headerBinaryTemp);
			
			outStream.flush();
			outStream.close();
			
			inputStream.close();
		}
		catch (UnsupportedEncodingException error)
		{
			
		}
		catch (IOException error)
		{
			
		}
		catch (Exception error)
		{
			
		}
		
		return header;
	}
	
	/**
	 * 파일 전송을 위한 헤더를 생성한다.
	 * @param fileSize 파일의 크기를 설정한다.
	 * @param fileName 파일의 명칭을 설정한다.
	 * @param headerBinary 헤더 바이너리를 참조한다.
	 * @param isLittleEndian Little Endian을 사용하는지 여부를 설정한다.
	 * @return 생성된 헤더가 반환된다.
	 */
	static Header generateHeader(long fileSize, String fileName, byte[] headerBinary, boolean isLittleEndian)
	{
		Header header = new Header(createPacketId(), fileSize, fileName);
		
		try
		{
			ByteArrayOutputStream outStream = new ByteArrayOutputStream(header._headerSize);
			
			outStream.write(_PACKET_SEPERATOR, 0, _PACKET_SEPERATOR.length);
			
			byte[] idArray = new String(header._id).getBytes("utf-8");
			
			outStream.write(idArray, 0, idArray.length);
			
			outStream.write(header.getPacketType().value);
			
			outStream.write(header.getEncryptionOption().value);
			
			ByteBuffer headerSizeByteBuffer = ByteBuffer.allocate(1024);
			
			headerSizeByteBuffer.put(BitConverter.getBytes(header.getHeaderSize()));
			
			byte[] headerSizeBinary = isLittleEndian ? headerSizeByteBuffer.order(ByteOrder.LITTLE_ENDIAN).array() : headerSizeByteBuffer.array();
			
			outStream.write(headerSizeBinary, 0, headerSizeBinary.length);
			
			ByteBuffer fileSizeByteBuffer = ByteBuffer.allocate(1024);
			
			fileSizeByteBuffer.put(BitConverter.getBytes(header.getFileSize()));
			
			byte[] fileSizeBinary = isLittleEndian ? fileSizeByteBuffer.order(ByteOrder.LITTLE_ENDIAN).array() : fileSizeByteBuffer.array();
			
			outStream.write(fileSizeBinary, 0, fileSizeBinary.length);
			
			ByteBuffer fileNameSizeByteBuffer = ByteBuffer.allocate(1024);
			
			fileNameSizeByteBuffer.put(BitConverter.getBytes(header.getFileNameSize()));
			
			byte[] fileNameSizeBinary = isLittleEndian ? fileNameSizeByteBuffer.order(ByteOrder.LITTLE_ENDIAN).array() : fileNameSizeByteBuffer.array();
			
			outStream.write(fileNameSizeBinary, 0, fileNameSizeBinary.length);
			
			ByteBuffer fileNameByteBuffer = ByteBuffer.allocate(1024);
			
			fileSizeByteBuffer.put(BitConverter.getBytes(header.getFileName()));
			
			byte[] fileNameBinary = isLittleEndian ? fileNameByteBuffer.order(ByteOrder.LITTLE_ENDIAN).array() : fileNameByteBuffer.array();
			
			outStream.write(fileNameBinary, 0, fileNameBinary.length);
			
			headerBinary = new byte[header.getHeaderSize()];
			
			ByteArrayInputStream inputStream = new ByteArrayInputStream(outStream.toByteArray());
			
			inputStream.read(headerBinary, 0, headerBinary.length);
			
			outStream.flush();
			outStream.close();
			
			inputStream.close();
		}
		catch (UnsupportedEncodingException error)
		{
			
		}
		catch (IOException error)
		{
			
		}
		catch (Exception error)
		{
			
		}
		
		return header;
	}
	
	/**
	 * 헤더 후보자에서 구분자의 위치를 찾는다.
	 * @param candidate 헤더 후보자를 설정한다.
	 * @return 헤더 후보자 내에서의 구분자의 위치를 반환한다. 구분자가 일치하는 구간이 존재하지 않을 경우 -1이 반환된다.
	 */
	static int SearchSeparatorFor(byte[] candidate)
	{
		int result = -1;
		
		boolean matched = false;
		
		if (candidate.length > 0 &&
				_PACKET_SEPERATOR.length > 0 &&
				candidate.length >= _PACKET_SEPERATOR.length)
		{
			// 후보자를 검색한다.
			for (int index1 = 0; index1 <= candidate.length - _PACKET_SEPERATOR.length; index1++)
			{
				// 구분자의 첫번째 아이템과 일치하는 구간(구분자와 일치하는 시작점)을 찾는다.
				if (candidate[index1] == _PACKET_SEPERATOR[0])
				{
					if (candidate.length > 1)
					{
						matched = true;
						
						// 구분자와 일치하는 시작점으로부터 구분자 전체와 비교한다.
						for (int index2 = 1; index2 <= _PACKET_SEPERATOR.length - 1; index2++)
						{
							if (candidate[index1 + index2] != _PACKET_SEPERATOR[index2])
							{
								matched = false;
								
								break;
							}
						}
						
						if (matched)
						{
							result = index1;
							
							break;
						}
					}
					// 후보자의 크기가 1보다 크지 않으면 종료 처리한다.
					else
					{
						result = index1;
						
						break;
					}
				}
			}
		}
		
		return result;
	}
	
	static Header ParseHeader(byte[] headerCandidate, HeaderParseResult result, boolean isLittleEndian)
	{
		Header header = null;
		
		try
		{
			// 헤더 후보자가 null이거나 크기가 0인 경우, 오류로 분류한다.
			if (headerCandidate == null || headerCandidate.length == 0)
			{
				result = HeaderParseResult.InvalidHeaderCandidate;
				
				return null;
			}
			
			int offset = 0;
			
			byte[] separator = new byte[_PACKET_SEPERATOR.length];
			
			System.arraycopy(headerCandidate, 0, separator, 0, separator.length);
			
			// 패킷 시작점이 존재하지 않는 경우, 오류로 분류한다.
			if (SearchSeparatorFor(headerCandidate) == -1)
			{
				result = HeaderParseResult.NoSeparator;
				
				return null;
			}
			
			offset += _PACKET_SEPERATOR.length;
			
			byte[] idBinary = new byte[_ID_CHAR_LENGTH];
			
			if (idBinary.length > headerCandidate.length)
			{
				result = HeaderParseResult.InvalidHeaderCandidate;
				
				return null;
			}
			
			System.arraycopy(headerCandidate, offset, idBinary, 0, _ID_CHAR_LENGTH);
			
			// 패킷 아이디를 읽어온다.
			char[] id = new String(idBinary, "utf-8").toCharArray();
			
			offset += _ID_CHAR_LENGTH;
			
			PacketType packetType = PacketType.Raw;
			
			if (!PacketType.convert(headerCandidate[offset], packetType))
			{
				result = HeaderParseResult.InvalidPacketType;
				
				return null;
			}
			
			offset ++;
			
			EncryptionOptions encryptionOption = EncryptionOptions.None;
			
			if (!EncryptionOptions.convert(headerCandidate[offset], encryptionOption))
			{
				result = HeaderParseResult.InvalidEncryptionOptions;
				
				return null;
			}
			
			offset++;
			
			int headerSize = BitConverter.toInt32(headerCandidate, offset);
			
			if (isLittleEndian)
			{
				headerSize = NetworkOrder.order(headerSize, ByteOrder.LITTLE_ENDIAN);
			}
			
			offset += Integer.SIZE;
			
			switch (packetType)
			{
				case Raw:
				case Serialized:
				case Message:
					
					// 본문의 크기를 읽어온다.
					int bodySize = BitConverter.toInt32(headerCandidate, offset);
					
					if (isLittleEndian)
					{
						bodySize = NetworkOrder.order(bodySize, ByteOrder.LITTLE_ENDIAN);
					}
					
					offset += Integer.SIZE;
					
					// 본문 데이터 타입의 크기를 읽어온다.
					int dataTypeSize = BitConverter.toInt32(headerCandidate, offset);
					
					if (isLittleEndian)
					{
						dataTypeSize = NetworkOrder.order(bodySize, ByteOrder.LITTLE_ENDIAN);
					}
					
					offset += Integer.SIZE;
					
					// 헤더의 크기가 올바르지 않은 경우, 오류로 분류한다.
					if (headerSize != _PACKET_SEPERATOR.length + _DEFAULT_HEADER_CLASS_SIZE + dataTypeSize)
					{
						result = HeaderParseResult.HeaderSizeNotMatch;
						
						return null;
					}
					
					// 본문 데이터 타입을 읽어온다.
					String dataType = Encoding.UTF8.getString(headerCandidate, offset, dataTypeSize);
					
					offset += dataTypeSize;
					
					header = new Header(id, bodySize, dataType, packetType, encryptionOption);
					
					break;
					
				case File:
					
					// 파일 크기를 읽어온다.
					long fileSize = BitConverter.toInt64(headerCandidate, offset);
					
					if (isLittleEndian)
					{
						fileSize = NetworkOrder.order(fileSize);
					}
					
					offset += Long.SIZE;
					
					int fileNameSize = BitConverter.toInt32(headerCandidate, offset);
					
					if (isLittleEndian)
					{
						fileNameSize = NetworkOrder.order(fileNameSize);
					}
					
					offset += Integer.SIZE;
					
					// 헤더 크기가 올바르지 않은 경우, 오류로 분류한다.
					if (headerSize != _PACKET_SEPERATOR.length + _FILE_HEADER_CLASS_SIZE + fileNameSize)
					{
						result = HeaderParseResult.HeaderSizeNotMatch;
						
						return null;
					}
					
					String fileName = Encoding.UTF8.getString(headerCandidate, offset, fileNameSize);
					
					offset += fileNameSize;
					
					header = new Header(id, fileSize, fileName);
					
					break;
			}
		}
		catch (Exception error)
		{
			
		}
		
		return header;
	}

	public char[] getId()
	{
		return this._id;
	}

	public PacketType getPacketType()
	{
		return this._packetType;
	}

	public EncryptionOptions getEncryptionOption()
	{
		return this._encryptionOption;
	}

	public int getHeaderSize()
	{
		return this._headerSize;
	}

	public int getBodySize()
	{
		return this._bodySize;
	}

	public int getDataTypesSize()
	{
		return this._dataTypesSize;
	}

	public String getDataType()
	{
		return this._dataType;
	}

	public long getFileSize()
	{
		return this._fileSize;
	}

	public int getFileNameSize()
	{
		return this._fileNameSize;
	}

	public String getFileName()
	{
		return this._fileName;
	}

	public int getChunkedIndex()
	{
		return this._chunkedIndex;
	}

	public int getChunkedSize()
	{
		return this._chunkedSize;
	}
}